import os
import yaml
import time
import json
import logging
import datetime
import requests
import subprocess
import threading
import tkinter as tk
from tkinter import messagebox
import platform
import re

# 移除了 TimedRotatingFileHandler 的导入

CONFIG_FILE = "config.yaml"
LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)


# ======================
# 1. 配置加载与保存
# ======================
def load_config():
    """从 YAML 文件加载配置 (Load configuration from a YAML file)"""
    try:
        # 使用 utf-8 编码加载文件
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return yaml.safe_load(f)
    except FileNotFoundError:
        # 如果文件不存在，返回默认配置
        return {
            "targets": ["www.baidu.com", "999.999.999.999"],
            "interval": 2,
            "ding_webhook": "https://oapi.dingtalk.com/robot/send?access_token=xxxx",
            "log_retention_days": 15
        }


def save_config(new_config):
    """保存配置到 YAML 文件 (Save configuration to a YAML file)"""
    # 确保保存时使用 utf-8 编码
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        yaml.dump(new_config, f, default_flow_style=False, allow_unicode=True)


# ======================
# 2. 监测线程 (MonitorThread)
# ======================
class MonitorThread(threading.Thread):
    def __init__(self, config):
        super().__init__()
        self.config = config
        # 使用 Event 来实现非阻塞的线程停止
        self._stop_event = threading.Event()
        # 【新增】：追踪每个目标的状态: True 正常, False 异常
        # 初始时，所有目标都视为正常 (避免首次运行时发送大量恢复通知)
        self.target_status = {target.strip(): True for target in config.get("targets", []) if target.strip()}

    # Ping 函数 (沿用原代码的跨平台兼容性修改)
    def ping_target(self, target):
        try:
            import platform
            if platform.system() == "Windows":
                # Windows: -n 2 (发送次数), -w 2000 (超时毫秒)
                cmd = ["ping", "-n", "2", "-w", "2000", target]
            else:
                # Linux/macOS: -c 2 (发送次数), -W 2 (超时秒)
                cmd = ["ping", "-c", "2", "-W", "2", target]

            result = subprocess.run(
                cmd,
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                shell=True
            )
            return result.returncode == 0
        except Exception:
            return False

    # 封装钉钉通知发送的底层逻辑
    def _send_ding_message_base(self, content):
        webhook = self.config.get("ding_webhook", "")

        if not webhook or "xxxx" in webhook:
            logging.error(
                f"❌ 钉钉 Webhook 未配置或配置错误，跳过通知！ （DingTalk Webhook not configured or configured incorrectly, skipping notification）")
            return False

        headers = {"Content-Type": "application/json"}
        data = {
            "msgtype": "text",
            "text": {"content": content}
        }

        try:
            response = requests.post(webhook, headers=headers, data=json.dumps(data), timeout=5)

            if response.status_code == 200:
                result = response.json()
                if result.get("errcode") == 0:
                    return True
                else:
                    logging.error(
                        f"❌ 钉钉 API 响应错误 （DingTalk API response error: {result}）. 可能是关键字或IP白名单问题！ （Check keywords or IP whitelist!）")
            else:
                logging.error(
                    f"❌ 发送钉钉消息失败 （Failed to send DingTalk message）. HTTP 状态码 / HTTP Status Code: {response.status_code}. 响应内容 / Response content: {response.text}")

        except requests.exceptions.Timeout:
            logging.error("❌ 发送钉钉消息超时 （Sending DingTalk message timed out）")
        except requests.exceptions.RequestException as e:
            logging.error(f"❌ 发送钉钉消息请求失败 （DingTalk request failed: {e}）")
        except Exception as e:
            logging.error(f"❌ 发送钉钉消息失败 （Failed to send DingTalk message: {e}）")
        return False

    # 钉钉通知函数：网络异常告警
    def send_ding_alert_message(self, target):
        # 钉钉消息内容：告警格式
        content = (
            f"🚨 网络异常告警：{target} 无法访问！请检查专线或网络连接。\n"
            f"----------------------------------------------------------------\n"
            f"🚨 Network Anomaly Alert: {target} Unreachable! Please check the dedicated line or network connection."
        )

        if self._send_ding_message_base(content):
            logging.info(f"通知发送成功 （Notification sent successfully to DingTalk for: {target}）")

    # 【新增】：钉钉通知函数：网络恢复通知
    def send_ding_recovery_message(self, target):
        # 钉钉消息内容：恢复格式
        content = (
            f"🟢 网络恢复通知：{target} 已恢复正常访问。\n"
            f"----------------------------------------------------------------\n"
            f"🟢 Network Recovery Notice: {target} is back online."
        )

        if self._send_ding_message_base(content):
            logging.info(f"通知发送成功 （Recovery notification sent successfully to DingTalk for: {target}）")

    def run(self):
        # 修复日志格式
        logging.info("🚀 监测线程启动 （Monitor thread started）")
        # 主循环：检查停止事件是否被设置
        while not self._stop_event.is_set():
            for target in self.config.get("targets", []):
                # 每循环一个目标后也检查一次停止信号
                if self._stop_event.is_set():
                    break

                target_str = target.strip()
                if not target_str:
                    continue

                # 确保字典中有当前目标的状态记录，如果没有，先设置为正常
                if target_str not in self.target_status:
                    self.target_status[target_str] = True

                # 获取当前目标的上次状态
                last_ok_status = self.target_status.get(target_str, True)
                current_ok = self.ping_target(target_str)

                if current_ok:
                    # ✅ 当前正常
                    logging.info(f"✅ {target_str} 正常 （{target_str} OK）")

                    # 【新增恢复逻辑】：如果上次状态是异常 (False)
                    if not last_ok_status:
                        logging.warning(f"🟢 {target_str} 网络已恢复！发送恢复通知。")
                        self.send_ding_recovery_message(target_str)
                        # 更新状态为正常
                        self.target_status[target_str] = True

                else:
                    # ❌ 当前异常
                    logging.error(f"❌ {target_str} 无法访问 （{target_str} Unreachable）！")

                    # 【新增告警逻辑】：如果上次状态是正常 (True)
                    if last_ok_status:
                        logging.warning(f"🚨 {target_str} 网络异常告警！发送告警通知。")
                        self.send_ding_alert_message(target_str)
                        # 更新状态为异常
                        self.target_status[target_str] = False

                        # 使用 wait 替代 sleep。如果 stop_event 被设置，wait会立即返回
            self._stop_event.wait(self.config.get("interval", 60))

        # 修复日志格式
        logging.info("监测线程已安全退出 （Monitor thread exited safely）")

    def stop(self):
        """设置停止事件，中断 wait()"""
        self._stop_event.set()


# ======================
# 3. 日志处理 (Log Handler)
# ======================
class TextHandler(logging.Handler):
    """将日志输出重定向到 Tkinter Text 组件 (Redirects logs to Tkinter Text widget)"""

    def __init__(self, text_widget):
        super().__init__()
        self.text_widget = text_widget
        self.text_widget.config(state=tk.DISABLED)

    def emit(self, record):
        msg = self.format(record)
        # Tkinter 操作必须在主线程中进行
        self.text_widget.after(0, self.append_log, msg)

    def append_log(self, msg):
        self.text_widget.config(state=tk.NORMAL)
        # 限制日志行数，避免内存过大
        if int(self.text_widget.index('end-1c').split('.')[0]) > 1000:
            self.text_widget.delete('1.0', '2.0')

        # 【核心修改】：根据消息内容判断是否为 ERROR/WARNING 级别，应用 tag
        tags = ()
        if "ERROR" in msg or "❌" in msg:
            tags = ('error_tag',)  # 红色标签
        elif "WARNING" in msg or "🟢" in msg or "🚨" in msg:
            tags = ('warning_tag',)  # 橙色/黄色标签 (新增)

        self.text_widget.insert(tk.END, msg + "\n", tags)
        self.text_widget.see(tk.END)
        self.text_widget.config(state=tk.DISABLED)


# 【新增函数】：用于清理旧日志文件
def clean_old_logs(retention_days):
    """根据配置保留天数清理 logs/ 目录下的旧日志文件"""
    # 计算截止日期
    cutoff_date = datetime.datetime.now() - datetime.timedelta(days=retention_days)

    # 日志文件名的日期匹配模式：monitor_YYYY-MM-DD.log
    date_pattern = re.compile(r"monitor_(\d{4}-\d{2}-\d{2})\.log$")

    # 遍历日志目录
    for filename in os.listdir(LOG_DIR):
        match = date_pattern.match(filename)
        if match:
            try:
                # 解析文件名中的日期
                file_date_str = match.group(1)
                file_date = datetime.datetime.strptime(file_date_str, "%Y-%m-%d")

                # 如果文件日期早于截止日期，则删除
                if file_date < cutoff_date:
                    file_path = os.path.join(LOG_DIR, filename)
                    os.remove(file_path)
                    logging.info(f"🗑️ 已清理过期日志文件: {filename}")
            except Exception as e:
                logging.error(f"清理日志文件 {filename} 失败: {e}")


# ======================
# 4. GUI 主程序 (App)
# ======================
class MonitorApp(tk.Tk):
    def __init__(self):
        super().__init__()

        # 【核心修改】：设置日志级别为 INFO
        # 默认级别是 WARNING，导致 INFO (例如 "✅ 正常") 日志被忽略
        logging.getLogger().setLevel(logging.INFO)

        # 修复窗口标题格式
        self.title("网络心跳检测工具 （Network Heartbeat Monitor）")
        self.monitor_thread = None
        self.current_log_date = datetime.date.today()  # 追踪当前的日志日期

        # 加载初始配置
        self.config = load_config()
        self.setup_logging()  # 在这里调用设置日志

        # 绘制 UI
        self.create_widgets()
        self.load_settings_to_ui()

        # 初始状态：监测停止，配置可编辑
        self.set_config_editable(True)

        # 窗口关闭时调用停止函数
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

        # 【核心新增】：设置每隔一分钟检查一次日期是否变化
        self.check_date_and_rotate_log()

        # 【新增功能】：软件启动自动开始检测
        self.start_monitor()

    def setup_logging(self):
        """设置日志记录器，添加文件Handler和GUI Handler，实现每日日志轮转"""
        # 清理所有旧的 FileHandler，但保留 StreamHandler 和 TextHandler
        for handler in logging.root.handlers[:]:
            if isinstance(handler, logging.FileHandler):
                logging.root.removeHandler(handler)
                handler.close()  # 关闭文件流

        formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")

        # 【核心修改 1】：不再更新 self.current_log_date
        # 始终根据 "今天" 的日期创建文件
        today_date = datetime.date.today()
        log_file = os.path.join(LOG_DIR, f"monitor_{today_date.strftime('%Y-%m-%d')}.log")

        # 使用普通的 FileHandler，模式 'a' (追加)
        file_handler = logging.FileHandler(log_file, encoding="utf-8")
        file_handler.setFormatter(formatter)

        # 重新添加 File Handler
        logging.getLogger().addHandler(file_handler)

        # 2. 检查并清理过期日志 (避免在每次启动时都清理，但这里可以保留)
        clean_old_logs(self.config.get("log_retention_days", 15))

    def check_date_and_rotate_log(self):
        """检查日期是否变化，如果变化则重新配置日志Handler"""
        today = datetime.date.today()

        if today > self.current_log_date:
            logging.info("🗓️ 检测到日期变化，正在轮转日志文件...")
            # 1. 重新配置日志 (这将关闭旧文件，打开新文件)
            self.setup_logging()

            # 2. 【核心修改 2】: 在 setup 成功后，更新 "当前" 日期
            self.current_log_date = today

            logging.info(f"✅ 日志已成功轮转至: monitor_{today.strftime('%Y-%m-%d')}.log")

        # 【核心修改】：将检查间隔从 60 秒缩短到 1 秒 (1000ms)
        # 避免在午夜时分出现长达 60 秒的延迟
        self.after(1000, self.check_date_and_rotate_log)

    def load_settings_to_ui(self):
        """将内存中的当前配置 (self.config) 强制加载到 UI 组件"""
        # 目标列表
        targets_str = "\n".join(self.config.get("targets", ["www.baidu.com"]))
        self.targets_text.config(state=tk.NORMAL)  # 临时设置为可编辑，以便插入文本
        self.targets_text.delete("1.0", tk.END)
        self.targets_text.insert(tk.END, targets_str)
        # 恢复状态将在 set_config_editable 中处理

        # 间隔
        self.interval_var.set(str(self.config.get("interval", 2)))

        # Webhook
        self.webhook_entry.config(state=tk.NORMAL)  # 临时设置为可编辑，以便插入文本
        self.webhook_entry.delete(0, tk.END)
        self.webhook_entry.insert(0, self.config.get("ding_webhook",
                                                     "https://oapi.dingtalk.com/robot/send?access_token=xxxx"))
        # 恢复状态将在 set_config_editable 中处理

    def create_widgets(self):
        # --- 配置框架 ---
        # 修复 LabelFrame 标题格式
        settings_frame = tk.LabelFrame(self, text="配置 （Settings）", padx=10, pady=10)
        settings_frame.pack(padx=10, pady=5, fill="x")

        # 设置第二列的权重，让其拉伸
        settings_frame.grid_columnconfigure(1, weight=1)

        # 1. 监测目标
        # 修复标签格式
        tk.Label(settings_frame, text="监测目标 (每行一个IP/域名) （Targets, one IP/Domain per line）:").grid(row=0,
                                                                                                            column=0,
                                                                                                            sticky="w",
                                                                                                            pady=2)
        # 设置 sticky="ew" 让 Text 组件拉伸
        self.targets_text = tk.Text(settings_frame, height=5, bd=1, relief="solid")
        self.targets_text.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="ew")

        # 2. 间隔
        # 修复标签格式
        tk.Label(settings_frame, text="检测间隔 (秒) （Interval in sec）:").grid(row=2, column=0, sticky="w", pady=2)
        self.interval_var = tk.StringVar(self)
        self.interval_entry = tk.Entry(settings_frame, textvariable=self.interval_var, width=10)  # 获取Entry引用
        self.interval_entry.grid(row=2, column=1, sticky="w", pady=2)  # 保持 sticky="w"，只占用左侧空间

        # 3. Webhook
        # 修复标签格式
        tk.Label(settings_frame, text="钉钉 Webhook （DingTalk Webhook）:").grid(row=3, column=0, sticky="w", pady=2)
        # 设置 sticky="ew" 让 Entry 组件拉伸
        self.webhook_entry = tk.Entry(settings_frame)
        self.webhook_entry.grid(row=4, column=0, columnspan=2, padx=5, pady=2, sticky="ew")

        # --- 按钮框架 ---
        button_frame = tk.Frame(self)
        button_frame.pack(pady=5)

        # 修复按钮文本格式
        self.start_button = tk.Button(button_frame, text="启动监测 （Start Monitor）", command=self.start_monitor,
                                      bg="#4CAF50",
                                      fg="white")
        self.start_button.pack(side=tk.LEFT, padx=10)

        # 修复按钮文本格式
        self.stop_button = tk.Button(button_frame, text="停止监测 （Stop Monitor）", command=self.stop_monitor,
                                     state=tk.DISABLED,
                                     bg="#F44336", fg="white")
        self.stop_button.pack(side=tk.LEFT, padx=10)

        # 修复按钮文本格式
        self.save_button = tk.Button(button_frame, text="保存配置 （Save Settings）", command=self.save_settings)
        self.save_button.pack(side=tk.LEFT, padx=10)

        # 修复按钮文本格式
        self.open_config_button = tk.Button(button_frame, text="打开配置文件所在位置 （Open Config Location）",
                                            command=self.open_config_location)
        self.open_config_button.pack(side=tk.LEFT, padx=10)

        # --- 日志框架 ---
        # 修复 LabelFrame 标题格式
        log_frame = tk.LabelFrame(self, text="运行日志 （Running Log）", padx=5, pady=5)
        log_frame.pack(padx=10, pady=5, fill="both", expand=True)

        self.log_text = tk.Text(log_frame, wrap=tk.WORD, height=15)
        self.log_text.pack(side=tk.LEFT, fill="both", expand=True)

        log_scrollbar = tk.Scrollbar(log_frame, command=self.log_text.yview)
        log_scrollbar.pack(side=tk.RIGHT, fill="y")
        self.log_text.config(yscrollcommand=log_scrollbar.set)

        # 【新增】：定义 ERROR/WARNING 标签样式 (红色/黄色高亮)
        self.log_text.tag_config('error_tag', foreground='red')
        self.log_text.tag_config('warning_tag', foreground='orange')  # 新增警告色

        # 4. 将日志重定向到 Text widget
        text_handler = TextHandler(self.log_text)
        text_handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
        # 必须在 self.log_text 创建后才能添加这个 handler
        logging.getLogger().addHandler(text_handler)

    def open_config_location(self):
        """打开配置文件 config.yaml 所在的文件夹 (跨平台)"""

        # 获取配置文件的完整路径
        config_path = os.path.abspath(CONFIG_FILE)

        # 获取文件所在的目录
        # 如果文件不存在，则打开当前工作目录
        dir_path = os.path.dirname(config_path) if os.path.exists(config_path) else os.getcwd()

        if not os.path.exists(dir_path):
            # 修复 messagebox 标题和内容格式
            messagebox.showerror("错误 （Error）", f"目录未找到 （Directory not found: {dir_path}）")
            return

        # 跨平台打开文件夹
        try:
            current_os = platform.system()
            if current_os == "Windows":
                # 在 Windows 上使用 explorer 打开文件夹
                subprocess.Popen(['explorer', dir_path])
            elif current_os == "Darwin":
                # 在 macOS 上使用 open 命令打开文件夹
                subprocess.call(['open', dir_path])
            elif current_os == "Linux":
                # 在 Linux 上使用 xdg-open/nautilus/kde-open 打开文件夹
                subprocess.call(['xdg-open', dir_path])
            else:
                # 修复 messagebox 标题和内容格式
                messagebox.showwarning("警告 （Warning）",
                                       f"当前操作系统 ({current_os}) 暂不支持快速打开文件夹 （Current OS does not support opening folder directly）")
        except Exception as e:
            # 修复 messagebox 标题和内容格式
            messagebox.showerror("打开失败 （Open Failed）", f"无法打开文件夹 （Cannot open folder: {e}）")

    def set_config_editable(self, editable):
        """控制配置输入框和保存按钮的编辑状态"""
        state = tk.NORMAL if editable else tk.DISABLED

        # 目标IP/域名 (Text widget)
        # 注意：Text widget 设置 DISABLED 后无法插入文本，所以要小心操作顺序
        self.targets_text.config(state=state)
        # 钉钉 Webhook (Entry widget)
        self.webhook_entry.config(state=state)
        # 检测间隔 (Entry widget)
        self.interval_entry.config(state=state)

        # 保存按钮状态
        self.save_button.config(state=state)

    def save_settings(self):
        """
        从 UI 收集数据并保存到文件和内存。
        只有点击这个按钮，配置才生效。
        """
        try:
            # 只有在可编辑状态下才能保存
            if self.targets_text.cget('state') == tk.DISABLED:
                # 修复 messagebox 标题和内容格式
                messagebox.showwarning("警告 （Warning）",
                                       "请先停止监测才能保存配置 （Stop monitoring first to save settings）")
                return

                # 获取目标列表并过滤空行
            targets_list = [t.strip() for t in self.targets_text.get("1.0", tk.END).splitlines() if t.strip()]
            interval = int(self.interval_var.get())

            if interval <= 0:
                # 修复异常信息格式
                raise ValueError("检测间隔必须大于0 （Interval must be greater than 0）")

            # 读取 log_retention_days 的现有值，确保保存时不会丢失
            current_log_retention_days = self.config.get("log_retention_days", 15)

            new_config = {
                "targets": targets_list,
                "interval": interval,
                "ding_webhook": self.webhook_entry.get().strip(),
                "log_retention_days": current_log_retention_days
            }
            # 写入文件
            save_config(new_config)

            # 【重要】：如果日志配置发生了变化，需要重新配置 logger
            if new_config.get("log_retention_days") != current_log_retention_days:
                self.setup_logging()  # 重新设置 logger

            # 更新内存中的配置
            self.config = new_config

            # 修复日志格式
            logging.info("✅ 配置已保存 （Settings saved）")

        except ValueError as e:
            # 修复 messagebox 标题和内容格式
            messagebox.showerror("配置错误 （Configuration Error）", f"配置信息无效 （Invalid configuration: {e}）")
        except Exception as e:
            # 修复 messagebox 标题和内容格式
            messagebox.showerror("保存失败 （Save Failed）", f"保存配置时发生错误 （Error saving settings: {e}）")

    def start_monitor(self):
        if self.monitor_thread and self.monitor_thread.is_alive():
            # 修复日志格式
            logging.warning("监测已在运行中 （Monitoring is already running）")
            return

        # 使用当前已保存的 self.config 来检查目标
        if not self.config.get("targets"):
            # 自动启动时，如果无目标，则不发出警告，而是静默退出
            if self.monitor_thread is None:
                # 修复日志格式
                logging.warning(
                    "当前配置中没有监测目标，自动启动已跳过。请手动保存目标后启动 （No monitoring targets in current config, auto-start skipped. Please save targets manually before starting）")
                return
            else:
                # 修复 messagebox 标题和内容格式
                messagebox.showwarning("警告 （Warning）",
                                       "当前配置中没有监测目标，请先保存目标 （No monitoring targets in current config. Please save targets first）！")
                return

        # 1. 创建并启动监测线程 (使用当前已保存的 self.config)
        self.monitor_thread = MonitorThread(self.config)
        self.monitor_thread.start()

        # 2. 禁用配置编辑和保存按钮
        self.set_config_editable(False)

        # 3. 重新加载配置到UI：将 self.config (已保存/运行的值) 覆盖到 UI 上，
        #    丢弃用户未保存的输入，并确保 UI 显示的内容与实际运行的配置一致。
        self.load_settings_to_ui()

        # 4. 启用/禁用监测按钮
        self.start_button.config(state=tk.DISABLED)
        self.stop_button.config(state=tk.NORMAL)

    def stop_monitor(self):
        # 1. 如果线程在运行
        if self.monitor_thread and self.monitor_thread.is_alive():
            # 2. 发出停止信号 (这会中断 sleep/wait)
            self.monitor_thread.stop()

            # 3. 彻底删除 join()，避免主线程等待子线程的退出。
            # 修复日志格式
            logging.info(
                "🛑 监测停止信号已发出，线程将在完成当前任务后退出 （Stop signal issued, thread will exit after current task）")

        # 4. 立即恢复配置编辑和按钮状态，保证界面不会卡死
        self.set_config_editable(True)
        self.start_button.config(state=tk.NORMAL)
        self.stop_button.config(state=tk.DISABLED)

    def on_closing(self):
        """关闭窗口时的处理 (Handler when closing the window)"""
        if self.monitor_thread and self.monitor_thread.is_alive():
            self.stop_monitor()
        self.destroy()


if __name__ == "__main__":
    app = MonitorApp()
    app.mainloop()